package Managers;

import Entities.Medium;
import Tools.DBTools;

import Tools.DatabaseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.*;
import org.xmldb.api.base.Collection;
import org.xmldb.api.modules.XMLResource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by Oliver Mrazik on 5. 6. 2014.
 * author: Oliver Mrazik
 * version: 2014-06-05
 */
public class MediumManagerImpl implements MediumManager{

    public static final Logger logger = LoggerFactory.getLogger(MediumManagerImpl.class);

    private String collection;
    private String login;
    private String pass;

    /**
     * Constructor which initialize database and collection
     * @param collection collection to initialize
     * @param name login to database
     * @param password password for database
     * @throws Tools.DatabaseException when error occurs while setting up driver
     */
    public MediumManagerImpl(String collection, String name, String password) throws DatabaseException {
        try {
            String driver = "org.exist.xmldb.DatabaseImpl";
            Class cl = Class.forName(driver);
            Database database = (Database) cl.newInstance();
            database.setProperty("create-database", "true");
            DatabaseManager.registerDatabase(database);

            this.collection = collection;
            this.login = name;
            this.pass = password;
        } catch (Exception ex) {
            logger.warn("Error initializing database connection.", ex);
            throw new DatabaseException("Error initializing database connection.", ex);
        }
    }

    /**
     * Method for access to database and return selected collection.
     * @return selected collection
     * @throws DatabaseException when error occurs when getting collection.
     */
    private Collection getColl() throws DatabaseException{
        try {
            String URI = "xmldb:exist://localhost:8899/exist/xmlrpc";
            //String URI = "xmldb:exist://localhost:8080/exist/xmlrpc";
            return DatabaseManager.getCollection(URI + this.collection, login, pass);
        } catch (XMLDBException ex) {
            logger.debug("Get collection error");
            throw new DatabaseException(ex);
        }
    }

    @Override
    public void createMedium(Medium medium) throws DatabaseException{
        //------------------------------------------------------------------------------------------------------------//
        //test for medium attributes
        if (medium == null) {
            logger.debug("Medium is null.");
            throw new NullPointerException("Object medium is null.");
        }

        if (medium.getId() != null) {
            logger.debug("Medium id should be null.");
            throw new DatabaseException("Medium id should be null.");
        }

        if (medium.getLabel() == null || medium.getType() == null || medium.getMovies() == null) {
            logger.debug("Medium is missing attribute.");
            throw new NullPointerException("Medium missing attributes.");
        }

        if (checkNames(medium.getLabel())) {
            logger.warn("Medium with label \"" + medium.getLabel() + "\" already exist.");
            throw new IllegalArgumentException("Medium with label \"" + medium.getLabel() + "\" already exist.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //get collection with mediums
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //create XML file & add it to database
        XMLResource resource = null;
        try {
            //--------------------------------------------------------------------------------------------------------//
            //get new id for medium
            try {
                medium.setId(col.createId());
            } catch (XMLDBException ex) {
                logger.warn("Error creating new id for medium.", ex);
                throw new DatabaseException("Error creating new id for medium.", ex);
            }
            //--------------------------------------------------------------------------------------------------------//

            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();

            Document doc = docBuilder.newDocument();
            Element root = doc.createElement("medium");
            doc.appendChild(root);

            Element movies = doc.createElement("movies");
            root.appendChild(movies);

            root.setAttribute("type", medium.getType());
            root.setAttribute("label", medium.getLabel());

            if (medium.getMovies().size() != 0) {
                for (String movie : medium.getMovies()) {
                    Element movieNode = doc.createElement("movie");
                    movieNode.setTextContent(movie);
                    movies.appendChild(movieNode);
                }
            }

            resource = (XMLResource) col.createResource(medium.getId(), "XMLResource");
            resource.setContentAsDOM(doc);
            col.storeResource(resource);
            logger.info("Resource successfully added to database.");
         //------------------------------------------------------------------------------------------------------------//

        } catch (ParserConfigurationException | XMLDBException ex) {
            logger.debug("Document creation error.", ex);
            throw new DatabaseException("Document creation error.", ex);
        } finally {
            DBTools.resourceClose(resource);
            DBTools.collectionClose(col);
        }
    }

    @Override
    public void deleteMedium(Medium medium) throws DatabaseException {
        //------------------------------------------------------------------------------------------------------------//
        //test for medium attributes
        testAttributes(medium);
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //get collection with mediums
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //delete resource
        XMLResource resource = null;
        try {
            resource = (XMLResource) col.getResource(medium.getId());
            if (resource == null) {
                logger.info("Medium \"" + medium.getLabel() + "\" is not in database.");
            } else {
                col.removeResource(resource);
            }
        } catch (XMLDBException ex) {
            logger.debug("Error deleting resource." ,ex);
            throw new DatabaseException("Error deleting resource.", ex);
        } finally {
             DBTools.resourceClose(resource);
             DBTools.collectionClose(col);
        }
        //------------------------------------------------------------------------------------------------------------//
    }

    @Override
    public void updateMedium(Medium medium) throws DatabaseException {

        //------------------------------------------------------------------------------------------------------------//
        //test for medium attributes
        testAttributes(medium);

        if (medium.getLabel() == null || medium.getType() == null || medium.getMovies() == null) {
            logger.debug("Medium is missing attribute.");
            throw new NullPointerException("Medium missing attributes.");
        }

        if (checkNames(medium.getLabel())) {
            logger.warn("Medium with label \"" + medium.getLabel() + "\" already exist.");
            throw new IllegalArgumentException("Medium with label \"" + medium.getLabel() + "\" already exist.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //get collection with mediums
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //update existing medium
        XMLResource resource = null;
        try {
            resource = (XMLResource) col.getResource(medium.getId());
            if (resource == null) {
                // this should never happen!
                logger.debug("Medium with label \"" + medium.getLabel() + "\" is not in database.");
            } else {
                Document doc = (Document) resource.getContentAsDOM();
                //old label -> new label
                doc.getFirstChild().getAttributes().getNamedItem("label").setNodeValue(medium.getLabel());

                //old type -> new type
                doc.getFirstChild().getAttributes().getNamedItem("type").setNodeValue(medium.getType());

                //old movies -> new movies
                Element delete = (Element) doc.getElementsByTagName("movies").item(0);
                delete.getParentNode().removeChild(delete);

                Element movies = doc.createElement("movies");
                doc.getFirstChild().appendChild(movies);

                if (medium.getMovies().size() != 0) {
                    for (String movie : medium.getMovies()) {
                        Element movieNode = doc.createElement("movie");
                        movieNode.setTextContent(movie);
                        movies.appendChild(movieNode);
                    }
                }

                resource.setContentAsDOM(doc);
                col.storeResource(resource);
                logger.info("Resource successfully updated.");
            }

        } catch (XMLDBException ex) {
            logger.debug("Document updating error.", ex);
            throw new DatabaseException("Document updating error.", ex);
        } finally {
            DBTools.resourceClose(resource);
            DBTools.collectionClose(col);
        }
        //------------------------------------------------------------------------------------------------------------//
    }

    @Override
    public Medium getMediumById(String id) throws DatabaseException {

        if (id == null) {
            logger.debug("id is null");
            throw new NullPointerException("Id is null.");
        }

        if (id.isEmpty() || id.equals("")) {
            logger.debug("id is empty");
            throw new IllegalArgumentException("Id is empty.");
        }

        //------------------------------------------------------------------------------------------------------------//
        //get collection with mediums
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //find and create new medium entity
        XMLResource resource = null;
        Medium ret = new Medium();
        try {
            resource = (XMLResource) col.getResource(id);
            if (resource == null) {
                logger.info("Medium with id \"" + id + "\" is not in database.");
                return null;
            }
            Document doc = (Document) resource.getContentAsDOM();

            //set id
            ret.setId(id);

            //set label
            ret.setLabel(doc.getFirstChild().getAttributes().getNamedItem("label").getNodeValue());

            //set type
            ret.setType(doc.getFirstChild().getAttributes().getNamedItem("type").getNodeValue());

            //set movies
            NodeList movies = doc.getElementsByTagName("movie");
            List<String> moviesList = new ArrayList<>();

            for (int i = 0; i < movies.getLength(); i++) {
                  moviesList.add(movies.item(i).getTextContent());
            }

            ret.setMovies(moviesList);

        } catch (XMLDBException ex) {
            logger.debug("Error creating new Medium entity", ex);
            throw new DatabaseException("Error creating new Medium entity", ex);
        } finally {
            DBTools.resourceClose(resource);
            DBTools.collectionClose(col);
        }
        //------------------------------------------------------------------------------------------------------------//
        return ret;
    }

    @Override
    public Medium getMediumByLabel(String label) throws DatabaseException {

        if (label == null) {
            logger.debug("Label is null");
            throw new NullPointerException("Label is null.");
        }

        if (label.isEmpty() || label.equals("")) {
            logger.debug("Label is empty");
            throw new IllegalArgumentException("Label is empty.");
        }

        for (Medium medium : getAllMediums()) {
            if (medium.getLabel().equals(label)) {
                return medium;
            }
        }

        return null;
    }

    @Override
    public List<Medium> getAllMediums() throws DatabaseException {

        List<Medium> result = new ArrayList<>();

        //------------------------------------------------------------------------------------------------------------//
        //get collection with mediums
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //gathering mediums
        try {
            String[] resources = col.listResources();
            for (int i = 0; i < col.getResourceCount(); i++) {
                //resources[i] = resources[i].substring(0, resources[i].length() - 4);
                result.add(getMediumById(resources[i]));
            }
        } catch(XMLDBException ex) {
            logger.debug("Error listing all mediums", ex);
            throw new DatabaseException("Error listing all mediums", ex);
        } finally {
            DBTools.collectionClose(col);
        }
        return result;
        //------------------------------------------------------------------------------------------------------------//
    }

    /**
     * Checks if medium label is not occupied by another medium.
     * @param newName tested name
     * @return true if label exist, false if not
     * @throws DatabaseException when fetching all mediums gets error.
     */
    private boolean checkNames(String newName) throws DatabaseException {
        List<String> names = new ArrayList<>();
        for (Medium medium : getAllMediums()) {
            names.add(medium.getLabel());
        }
        return names.contains(newName);
    }

    /**
     * Method test attributes of medium.
     * @param medium tested medium
     */
    private void testAttributes(Medium medium) {

        if (medium == null) {
            logger.debug("Medium is null.");
            throw new NullPointerException("Object medium is null.");
        }

        if (medium.getId() == null) {
            logger.debug("Medium id is null.");
            throw new NullPointerException("Medium id is null.");
        }
    }
}
